Published on

ShapeStyle In SwiftUI

Authors
  • Name
    Twitter

ShapeStyle 是什么

在 SwiftUI 中我们会经常在使用一些 ViewModifier 的时候,比如 .border 的时候。会传递一个参数,这个参数的类型是Shape。其中,.border的完整定义如下:

@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
extension View {

    /// Layers a secondary view in front of this view.
    ///
    /// When you apply an overlay to a view, the original view continues to
    /// provide the layout characteristics for the resulting view. In the
    /// following example, the heart image is shown overlaid in front of, and
    /// aligned to the bottom of the folder image.
    ///
    ///     Image(systemName: "folder")
    ///         .font(.system(size: 55, weight: .thin))
    ///         .overlay(Text("❤️"), alignment: .bottom)
    ///
    /// ![View showing placement of a heart overlaid onto a folder
    /// icon.](View-overlay-1)
    ///
    /// - Parameters:
    ///   - overlay: The view to layer in front of this view.
    ///   - alignment: The alignment for `overlay` in relation to this view.
    ///
    /// - Returns: A view that layers `overlay` in front of the view.
    @available(iOS, introduced: 13.0, deprecated: 100000.0, message: "Use `overlay(alignment:content:)` instead.")
    @available(macOS, introduced: 10.15, deprecated: 100000.0, message: "Use `overlay(alignment:content:)` instead.")
    @available(tvOS, introduced: 13.0, deprecated: 100000.0, message: "Use `overlay(alignment:content:)` instead.")
    @available(watchOS, introduced: 6.0, deprecated: 100000.0, message: "Use `overlay(alignment:content:)` instead.")
    @available(visionOS, introduced: 1.0, deprecated: 100000.0, message: "Use `overlay(alignment:content:)` instead.")
    @inlinable nonisolated public func overlay<Overlay>(_ overlay: Overlay, alignment: Alignment = .center) -> some View where Overlay : View


    /// Adds a border to this view with the specified style and width.
    ///
    /// Use this modifier to draw a border of a specified width around the
    /// view's frame. By default, the border appears inside the bounds of this
    /// view. For example, you can add a four-point wide border covers the text:
    ///
    ///     Text("Purple border inside the view bounds.")
    ///         .border(Color.purple, width: 4)
    ///
    /// ![A screenshot showing the text Purple border inside the view bounds.
    /// The text is surrounded by a purple border that outlines the text,
    /// but isn't quite big enough and encroaches on the text.](View-border-1)
    ///
    /// To place a border around the outside of this view, apply padding of the
    /// same width before adding the border:
    ///
    ///     Text("Purple border outside the view bounds.")
    ///         .padding(4)
    ///         .border(Color.purple, width: 4)
    ///
    /// ![A screenshot showing the text Purple border outside the view bounds.
    /// The text is surrounded by a purple border that outlines the text
    /// without touching the text.](View-border-2)
    ///
    /// - Parameters:
    ///   - content: A value that conforms to the ``ShapeStyle`` protocol,
    ///     like a ``Color`` or ``HierarchicalShapeStyle``, that SwiftUI
    ///     uses to fill the border.
    ///   - width: The thickness of the border. The default is 1 pixel.
    ///
    /// - Returns: A view that adds a border with the specified style and width
    ///   to this view.
    @inlinable nonisolated public func border<S>(_ content: S, width: CGFloat = 1) -> some View where S : ShapeStyle

}

border可以用在哪些类型上面

我们可以发现这个方法是定义在 extension View 中,那么是不是所有的满足 View 协议的类型都可以调用这个方法呢?

答案是肯定的

对协议的扩展添加限制的两种情况

这里的 extension View 加上限制条件。常见的限制条件有两种情况:

  1. Self类型约束
  2. where限定条件

比如下面的代码示例,针对上面的两种情况的举例:

Self限定

protocol Animal {}

extension Animal where Self: Equatable {
    func isSame(as other: Self) -> Bool {
        return self == other
    }
}

struct Dog: Animal, Equatable {
    let name: String
}

struct Cat: Animal {
    let name: String
}

let d1 = Dog(name: "旺财")
let d2 = Dog(name: "旺财")
print(d1.isSame(as: d2))  // ✅ 可以用,因为 Dog: Equatable

let c1 = Cat(name: "小花")
// c1.isSame(as: c1) ❌ 编译报错,因为 Cat 没有实现 Equatable

对关联类型加以限制

protocol Container {
    associatedtype Item
    var items: [Item] { get }
}

extension Container where Item == Int {
    func sum() -> Int {
        items.reduce(0, +)
    }
}

struct IntBox: Container {
    var items: [Int]
}

struct StringBox: Container {
    var items: [String]
}

let box = IntBox(items: [1, 2, 3])
print(box.sum())   // ✅ 可以

let sbox = StringBox(items: ["a", "b"])
// sbox.sum() ❌ 不行,因为 Item 不是 Int

对其定义的语法分析

@inlinable nonisolated public func border<S>(_ content: S, width: CGFloat = 1) -> some View where S : ShapeStyle

在 SwiftUI 中,@inlinable 的主要作用是 让常用 modifier 方法在跨模块调用时也能被内联,从而提升性能,减少开销。

nonisolated

在 Swift Concurrency 中 Actor 内部的方法和属性是隔离的 (isolated),外部想要访问这些方法和属性,需要使用 await 异步调用,保证其线程安全。

有些时候,我们希望某些 不依赖 actor 内部状态的方法 不需要异步/不受 actor 隔离,这时,我们可以在方法前面加上 nonisolated

actor Counter {
    private var value = 0
    
    // 普通方法 -> 需要 await
    func increment() { value += 1 }
    
    // 不依赖内部状态 -> 可以 nonisolated
    nonisolated func version() -> String {
        "1.0"
    }
}

let c = Counter()
await c.increment()     // 需要 await
print(c.version())      // 不需要 await,直接同步调用

SwiftUI 中的 nonisolated

为什么要 nonisolated? SwiftUI 的 View 并不是 actor,但 Swift 现在在很多地方支持 Sendable / actor 安全推导。 标记 nonisolated 是为了告诉编译器: 这个方法是纯粹的修饰器,不会依赖并发上下文,也不访问任何 actor 隔离的状态。 这样你就可以在任何并发环境里自由调用 .border(...),不需要 await。

泛型和泛型约束

我们希望这个方法中的参数类型是一个符合

如果是让我们来定义这个 ViewModifier, 我大概会这样定义:


@inlinable nonisolated public func border(content: Content, width: CGFloat) -> some View {

}

这里,我们需要对边框的内容填充增加限定。其中的 Content 我们可以用泛型思想。

@inlinable nonisolated public func border<S>(_ content:S, width: CGFloat = 1) -> some View where S : ShapeStyle {

}

通过在方法名后面加上<S>来定义一个泛型,这个泛型是用来约定我们参数 content 的类型,在方法后面采用 S: ShapeStyle 来限定其类型S必须遵循 ShapeStyle

这里我们需要回去复习一下 TSLP 中相关的内容:

  1. 方法的参数标签
  2. 参数的默认值和可选参数 https://docs.swift.org/swift-book/documentation/the-swift-programming-language/functions
  3. some View 是什么类型 Opaque Type
  4. where 的用法和作用 Generic where